﻿Imports System.Threading
Imports log4net
Imports System.Net.NetworkInformation
Imports VPager.Enumerations


''' <summary>
''' This class acts as a simple network integrity monitor.  It monitors network integrity utilizing the
''' .NET "My.Computer.Network" API, most importantly the Ping functionality.  By periodically
''' "pinging" one or more remote hosts basic network availability can be determined.
''' Configurable settings for netowrk monitoring are: Host LAN Targets, Host WAN Targets, Ping Timeout, Ping Frequency,
''' 
''' Test Results                                         Status
''' ============================================================
''' Network is Available and Ping Succeeds               [Okay]
''' Network is Unavailable                               [Error]
''' Internet/WAN Ping Fails                              [WARNING]
''' Local/LAN Ping Fails                                 [ERROR] 
''' </summary>
''' <remarks></remarks>
Public Class NetworkMonitor

    ' Some default values
    Public Const DEF_PING_TIMEOUT = 3000 ' Milliseconds
    Public Const MIN_PING_FREQUENCY = 3  ' Seconds
    Public Const DEF_WAN_PING_TARGETS = "www.fellowshipone.com"

    Private Shared sLog As ILog

    ' Network Status Values
    'Public Enum NetworkStatus
    '    UNKNOWN = -1
    '    NORMAL = 0
    '    DEGRADED = 1
    '    [ERROR] = 2
    'End Enum

    ' -------------------------------------------------------------------------
    ' Member variables
    ' -------------------------------------------------------------------------
    '
    ' The main worker thread so that monitoring happens in the backgound
    Private mWorker As Thread

    ' Tracks the current network status
    Private mStatus As HealthState = HealthState.UNKNOWN

    ' Tracks the start time when a network ping fails.
    Private mDownStart As DateTime = Nothing

    ' How often in seconds to perform ping test
    Private mTestFrequency As Long

    ' How long in seconds can a network outage last before we raise the status to Error.
    Private mErrorThreshold As Long

    ' Message associated with current status if any
    Private mStatusMessage As String = ""

    ' How long in miliseconds before the ping test times out with a failure
    Private mPingTimeout As Long

    ' A single comma seperated string of Local (LAN) target hosts to ping
    Private mLanPingTargets As String

    ' A single comma seperated string of Internet (WAN) target hosts to ping
    Private mWanPingTargets As String

    Private mContinueMonitoring As Boolean = True

    Private mNextTestTime As DateTime = Nothing

    ' -------------------------------------------------------------------------
    ' Event Definitions
    ' -------------------------------------------------------------------------
    '
    ' This event is fired whenever the Network status changes
    Public Event StatusChanged(ByVal sender As NetworkMonitor, ByVal e As NetworkEventArgs)

    ' -------------------------------------------------------------------------
    ' Property Definitions
    ' -------------------------------------------------------------------------
    '
    ''' <summary>
    ''' Status Property returns current netowrk status
    ''' </summary>
    Public ReadOnly Property Status As HealthState
        Get
            Return mStatus
        End Get
    End Property

    ''' <summary>
    ''' StatusMessage Property returns current message text assocaited with Status
    ''' </summary>
    Public ReadOnly Property StatusMessage As String
        Get
            Return mStatusMessage
        End Get
    End Property

    ''' <summary>
    ''' PingTimeout Property can be set to determine how long a Ping test should wait before
    ''' indicating a failure.  
    ''' </summary>
    ''' <value>Timeout given as milliseconds.</value>
    Public Property PingTimeout As Long
        Get
            Return mPingTimeout
        End Get
        Set(ByVal value As Long)
            mPingTimeout = value
        End Set
    End Property

    ''' <summary>
    ''' A single string of comma seperate LAN hosts to ping
    ''' </summary>
    ''' <value>String of target hosts</value>
    Public Property LanPingTargets As String
        Get
            Return mLanPingTargets
        End Get
        Set(ByVal value As String)
            mLanPingTargets = value
        End Set
    End Property

    ''' <summary>
    ''' A single string of comma seperate WAN hosts to ping
    ''' </summary>
    ''' <value>String of target hosts</value>
    Public Property WanPingTargets As String
        Get
            Return mWanPingTargets
        End Get
        Set(ByVal value As String)
            mWanPingTargets = value
        End Set
    End Property

    ' -------------------------------------------------------------------------
    ' Main Code
    ' -------------------------------------------------------------------------

    ''' <summary>
    ''' Contructor.
    ''' </summary>
    ''' <param name="pingFrequency">How often in seconds to test the network via Ping tests.</param>
    ''' <param name="alarmThreshold">How long to wait in seconds since last Ping failure before 
    ''' elevating network status from Warning to Error. </param>
    ''' <remarks>The startMonitoring() method must be called after the creation of new NetworkMonitor 
    ''' object to begin the actual monitoring.</remarks>
    Public Sub New(ByVal pingFrequency As Long, ByVal alarmThreshold As Long)
        sLog = LogManager.GetLogger(Me.GetType)

        If (pingFrequency < MIN_PING_FREQUENCY) Then
            mTestFrequency = MIN_PING_FREQUENCY
        Else
            mTestFrequency = pingFrequency
        End If
        mNextTestTime = Date.Now

        mErrorThreshold = alarmThreshold

        ' This the main worker background thread which will be started when startMonitoring() is called.
        ' The 'doMonitoring' method is what the Thread will call and run when started.
        mWorker = New Thread(AddressOf doMonitoring)
        sLog.Debug("A Network Monitor instance created.")
    End Sub

    Public Sub startMontoring()
        checkConfiguration()
        mWorker.Start()
        sLog.Debug("Network Monitoring has started.")
    End Sub

    Sub stopMonitoring()
        mContinueMonitoring = False
        sLog.Debug("Network Monitoring has stopped.")
    End Sub


    ''' <summary>
    ''' Private method to set the current Network Status.  Primary purpose of this method is to
    ''' detect a change in Status and fire off an Event to Listeners when a change occurs.
    ''' </summary>
    ''' <param name="status"></param>
    ''' <param name="message"></param>
    Private Sub setStatus(ByVal status As HealthState, ByVal message As String)
        If status <> mStatus Then
            ' status has changed
            sLog.Debug("Network status has changed to: " + status.ToString)
            mStatus = status
            mStatusMessage = message
            onStatusChanged(New NetworkEventArgs(Me.Status, Me.StatusMessage))
        End If
    End Sub

    ''' <summary>
    ''' Handles the firing of the internal event to any Listeners.
    ''' </summary>
    ''' <param name="e"></param>
    ''' <remarks></remarks>
    Private Sub onStatusChanged(ByVal e As NetworkEventArgs)
        RaiseEvent StatusChanged(Me, e)
    End Sub

    ''' <summary>
    ''' Handles the Ping test across one or more Ping host targets
    ''' </summary>
    ''' <returns>True if any of the hosts can be Ping'ed prior to a timeout.</returns>
    ''' <remarks>Host are taken from the PingTargets Property</remarks>
    Private Function canPingTargets(ByVal hostString As String) As Boolean
        Dim hostReached As Boolean = False
        Dim targets As String() = Split(hostString, ",")
        Try
            For Each host As String In targets
                hostReached = My.Computer.Network.Ping(host, mPingTimeout)
                ' Just need one good Ping.
                If hostReached Then
                    Exit For
                End If
            Next
        Catch ex As Exception
            hostReached = False
            sLog.Warn("Ping Exception Thrown. Network or Interface may be down.")
        End Try

        Return hostReached

    End Function

    ''' <summary>
    ''' The main monitoring method which is invoked by the worker thread. The method contains
    ''' a infinite while loop so as to never release the worker thread from monitoring until
    ''' the NetworkMonitor object instance is destroyed.
    ''' </summary>
    ''' <remarks>Network Status is determined and set from this mentod.</remarks>
    Private Sub doMonitoring()
        While mContinueMonitoring
            ' First perform a basic Network availability test
            'sLog.Debug("Testing network availability.")
            If My.Computer.Network.IsAvailable Then
                ' Nest try to Ping the host tragets provided
                If canPingTargets(mLanPingTargets) Then
                    ' Ping Succeeded. Local Network is good
                    If canPingTargets(mWanPingTargets) Then
                        ' Ping Succeeded. Internet is reachable.
                        mDownStart = Nothing
                        setStatus(HealthState.NORMAL, "Local Network and Internet are reachable.")
                    Else
                        ' Keep Status level at Warning
                        setStatus(HealthState.DEGRADED, "Local Network is reachable but Internet has become unreachable.")
                    End If

                Else
                    ' Ping Failed. Network is unreachable.
                    setStatus(HealthState.[ERROR], "Network is available but Local Network has become unreachable!")
                End If
            Else
                ' Basic Network Availibility Test Failed.  Set Status Level to Error.
                setStatus(HealthState.[ERROR], "Network has become unavailable!")
            End If

            ' Get next test time
            mNextTestTime = Date.Now.AddSeconds(mTestFrequency)

            ' Wait then test again.
            ' Go into a loop of short sleep cycles waiting for next test time.
            ' Short sleep cycles allow the thread to terminate sooner when application exits.
            Do While (Date.Now.CompareTo(mNextTestTime) < 0 And mContinueMonitoring)
                'sLog.Debug("Sleeping...")
                Thread.Sleep(1000)
                'sLog.Debug("Checking...")
            Loop

        End While

    End Sub

    ''' <summary>
    ''' Ensures every configuration settings has a valid value.  Preference is to use values
    ''' stored in the application settings, followed by defined defaults.
    ''' </summary>
    ''' <remarks></remarks>
    Sub checkConfiguration()

        If mPingTimeout < 0 Then
            mPingTimeout = DEF_PING_TIMEOUT
        End If
        sLog.Info("Using [Ping Time Out] = " + mPingTimeout.ToString + " seconds.")

        If (mWanPingTargets Is Nothing OrElse mWanPingTargets = "") Then
            mWanPingTargets = DEF_WAN_PING_TARGETS
        End If
        sLog.Info("Using [WAN Ping Targets] = " + mWanPingTargets)

        If (mLanPingTargets Is Nothing OrElse mLanPingTargets = "") Then
            mLanPingTargets = getDefaultGateway()
            sLog.Debug("Using Default Gateway as LAN Ping Target")
        End If
        sLog.Info("Using [LAN Ping Targets] = " + mLanPingTargets)

    End Sub

    ''' <summary>
    ''' Returns the IP address for the Default Gateway associated with local machine.
    ''' </summary>
    ''' <remarks></remarks>
    Private Function getDefaultGateway() As String
        'Display Default Gateway of local machine
        Dim gatewayIP As String = Nothing
        Dim networkAdapters() As NetworkInterface = NetworkInterface.GetAllNetworkInterfaces
        Dim gateways As GatewayIPAddressInformationCollection = Nothing

        For Each myNetworkAdapter As NetworkInterface In networkAdapters
            gateways = myNetworkAdapter.GetIPProperties.GatewayAddresses
            For Each gateway As GatewayIPAddressInformation In gateways
                If gateway.Address IsNot Nothing Then
                    gatewayIP = gateway.Address.ToString()
                    Exit For
                End If
            Next
        Next

        Return gatewayIP
    End Function

    ' Inner Class To Handle Event Args
    Public Class NetworkEventArgs
        Inherits EventArgs

        Private mStatus As HealthState = HealthState.NORMAL
        Private mMessage As String = ""
        Private mTime As DateTime

        Public ReadOnly Property Status As HealthState
            Get
                Return mStatus
            End Get
        End Property

        Public ReadOnly Property Message As String
            Get
                Return mMessage
            End Get
        End Property

        Public ReadOnly Property Time As DateTime
            Get
                Return mTime
            End Get
        End Property

        Public Sub New(ByVal status As HealthState, ByVal message As String)
            Me.New(status, message, DateTime.Now)
        End Sub

        Private Sub New(ByVal status As HealthState, ByVal message As String, ByVal time As Date)
            MyBase.New()
            mStatus = status
            mMessage = message
            mTime = time
        End Sub

    End Class

End Class
